package Dungeon;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import ConditionPackage.Condition;
import ConditionPackage.DefaultCondition;
import ConditionPackage.RockCondition;
import SquarePackage.Direction;
import SquarePackage.Rock;
import SquarePackage.Square;
import SquarePackage.TeleportableSquare;
import SquarePackage.Temperature;
import SquarePackage.TransparentSquare;
import be.kuleuven.cs.som.annotate.*;

/**
 * Class for a dungeon.
 * 
 * @invar	All coordinates are positive and smaller than the maximum value.
 * 			| isValidDimension(dimension)
 * 
 * @author 	Brecht Reynders en Florian Hendrickx
 * @version 1.0
 */
public abstract class Subdungeon<E extends Square> {
	
	/////////////////////////  
	//////CONSTRUCTOREN//////
	/////////////////////////  
	/**
	 * Initializes a new dungeon with the given coordinates for the maximum coordinate
	 * 
	 * @param	maxX
	 * 			The maximum x-coordinate value.
	 * @param	maxY
	 * 			The maximum y-coordinate value.
	 * @param	maxZ
	 * 			The maximum z-coordinate value.
	 * @post	The given coordinates are the coordinates of the maximum dimension.
	 * 			| Dimension(maxX, maxY, maxZ) == new.getMaxDimension() 
	 */
	public Subdungeon(long maxX, long maxY, long maxZ){
		this(new Dimension(maxX, maxY, maxZ));
	}
	/**
	 * Initializes a new dungeon with the given dimension as maximum dimension.
	 * 
	 * @param 	maxDimension
	 * 			The maximum dimension
	 * @post	The given maximum dimension is the new max dimension.
	 * 			| new.getMaxDimension = maxDimension
	 */
	public Subdungeon(Dimension maxDimension){
		this.maxDimension = maxDimension;
		setMaxDimension(maxDimension);
	}
	
	///////////////////////
	//////VARIABLES////////
	///////////////////////
	/**
	 * Variable registering the maximum position.
	 */
	private Dimension maxDimension;
	
	/**
	 * Variable registering the containing dungeon.
	 */
	private ComposedDungeon<?,?,Subdungeon<?>> containingDungeon;
	
	/**
	 * Variable registering where this subdungeon starts in the containing dungeon.
	 */
	private Dimension startingDimension;
	
	/////////////////////////////
	//////MAXIMUM DIMENSION//////
	/////////////////////////////
	/**
	 * Return the maximum dimension.
	 */
	@Basic @Raw
	public Dimension getMaxDimension(){
		return maxDimension;
	}
	
	/**
	 * Sets the maximum dimension of the dungeon to the given maximum dimension.
	 * 
	 * @param 	newMaxDimension
	 * 			The new wanted maximum dimension.
	 * @post	The given maximum dimension is the new maximum dimension.
	 * 			| new.maximumDimension == newMaxdimension
	 * @throws 	IllegalArgumentException
	 * 			The given new maximum is not a valid dimension.
	 * 			| !isValidNewMaxDimension(newMaxDimension)
	 */
	@Model
	public void setMaxDimension(Dimension newMaxDimension) throws IllegalArgumentException{
		if(!isValidNewMaxDimension(newMaxDimension))
			throw new IllegalArgumentException("Not a valid maximum dimension");
		maxDimension = newMaxDimension;
	}

	/**
	 * Checks whether the given dimension is a valid new maximum dimension.
	 * 
	 * @param 	dimension
	 * 			The new wanted dimension.
	 * @return	True if and only if the dimension is larger than the previous maximum dimension
	 * 			| result = dimension.getXPosition() >= getMaxDimension().getXPosition() && 
	 * 			|	dimension.getYPosition() >= getMaxDimension().getYPosition() && 
	 * 			| 	dimension.getZPosition() >= getMaxDimension().getZPosition() && 
	 * 			|	//TODO
	 */
	protected boolean isValidNewMaxDimension(Dimension dimension){ //TODO
		if (dimension == null)
			return false;
		if (!(dimension.getXPosition() >= getMaxDimension().getXPosition() && dimension.getYPosition() >= getMaxDimension().getYPosition() && dimension.getZPosition() >= getMaxDimension().getZPosition()))
			return false;
		if(hasContainingDungeon()){
			Dimension startingDimension = getStartDimensionInContainingdungeon();
			for(Dimension usedDimension: getAllPossibleDimensions(getMaxDimension(), dimension)){
				usedDimension = usedDimension.add(startingDimension);
				if(!usedDimension.isSmallerThan(getContainingDungeon().getMaxDimension()))
					return false;
				if(!getContainingDungeon().checkIsValidExtension(usedDimension))
					return false;
			}
		}
		return true;
	}
		
	/**
	 * Return all the new dimension that are created if the dungeon with maximum dimension gets set with the given new maximum dimension.
	 * 
	 * @param 	dimension 
	 * 			Old space used in the dungeon.
	 * @param	maxDimension
	 * 			The new wanted maximum space for this dungeon.
	 * @return	A list with all the dimension extra used in the dungeon.
	 */
	protected Set<Dimension> getAllPossibleDimensions(Dimension dimension, Dimension maxDimension){ //TODO
		Set<Dimension>dimensions = new HashSet<Dimension>();
		for(long x = 0; 0<=x && x<=maxDimension.getXPosition(); x++){
			for(long y = 0; 0<=y && y<= maxDimension.getYPosition(); y++){
				for(long z =0; 0<=z && z<= maxDimension.getZPosition(); z++){
					if(!(new Dimension(x,y,z).isSmallerThan(dimension))){
						dimensions.add(new Dimension(x,y,z));
					}
				}
			}
		}
		return dimensions;
	}
	
	////////////////////////////////////
	///////ROOT- AND SUBDUNGEONS////////
	////////////////////////////////////
	/**
	 * Returns the containing dungeon.
	 */
	public  ComposedDungeon<?, ?,Subdungeon<?>>  getContainingDungeon(){
		return containingDungeon;
	}
	
	/**
	 * Returns the root dungeon.
	 * 
	 * @return	The root dungeon of the containing dungeon, if it has a root dungeon, else this is the root dungeon.
	 * 			| if(getContainingDungeon() == null)
	 * 			| return this
	 * 			| return getContainingDungeon().getRootDungeon()
	 */
	public Subdungeon<? super E> getRootDungeon(){
		if(!hasContainingDungeon())
			return this;
		return (Subdungeon<? super E>) getContainingDungeon().getRootDungeon();
	}
	
	/**
	 * Returns all the levels and shafts in this dungeon.
	 */
	public abstract Collection<Dungeon<? extends E>> getAllLevelsAndShafts();
	
	/**
	 * Returns the starting dimension of this dungeon in the containing dungeon.
	 */
	protected Dimension getStartDimensionInContainingdungeon(){
		return startingDimension;
	}
	
	/**
	 * Checks whether this dungeon has a containing dungeon.
	 * 
	 * @return	True if and only if this dungeon has a containing dungeon.
	 * 			| result = containingDungeon != null
	 */
	protected boolean hasContainingDungeon(){
		return containingDungeon != null;
	}
	
	/**
	 * Sets the given subdungeon as the containing dungeon.
	 * 
	 * @param 	composedDungeon
	 * 			The wanted containing dungeon.
	 * @param	startingDimension
	 * 			The dimension of this dungeon in the containing dungeon.
	 * @post	The given containing dungeon is the new containing dungeon.
	 * 			| new.getContainingDungeon() = composedDungeon
	 * @post	The given starting dimension is the new starting dimension.
	 * 			| new.getStartingDimension() = startingDimension
	 * @throws 	IllegalArgumentException
	 * 			This dungeon already has a containing dungeon.
	 * 			| hasContainingDungeon()
	 */ 
	protected void setContainingDungeon(ComposedDungeon<?, ?, Subdungeon<?>> composedDungeon, Dimension startingDimension)throws IllegalArgumentException{
		if(hasContainingDungeon())
			throw new IllegalArgumentException("This dungeon already has " + getContainingDungeon() + " as containing dungeon.");
		containingDungeon = composedDungeon;
		this.startingDimension = startingDimension;
	}
	
	/**
	 * Returns the given dimension in absolute coordinates of the rootdungeon.
	 * 
	 * @param 	dimension `
	 * 			The relative dimension in this dungeon.
	 * @return	If this method is not called in its root dungeon, another dimension which represents its absolute dimension.
	 */
	public abstract Dimension getRootDimension(Dimension dimension);
	
	/////////////////////
	////////GETTERS//////
	/////////////////////
	/**
	 * Returns the dimension of the given square in this dungeon.
	 * 
	 * @param 	square
	 * 			The square of which the dimension should be returned.
	 * @return	The dimension in this dungeon where the square is located.
	 */
	public abstract Dimension getDimensionOf(E square);
	
	/**
	 * Returns the square at the given dimension.
	 * 
	 * @param 	dimension
	 * 			The dimension of which the square should be returned.
	 * @return	The square at the given dimension.
	 */
	public abstract E getSquareAt(Dimension dimension);
	
	/**
	 * Returns the room that contains the given square.
	 * 
	 * @param	square
	 * 			The square of which the room should be returned.
	 * @return	The room of this square.
	 */
	public abstract Room<? extends E> getRoomOf(Square square);
	
	/**
	 * Returns a collection of all the dimension used in this dungeon.
	 */
	public abstract Collection<Dimension> getUsedDimensions();
	
	//////////////////////////////////
	///////MANIPULATING SQUARES///////
	//////////////////////////////////
	/**
	 * Add the given square on the given dimension.
	 * 
	 * @param 	square
	 * 			The square to add.
	 * @param 	dimension
	 * 			The dimension where the square should be added.
	 * @post	The square is added at the given dimension.
	 * 			| getAllSquares().contains(square)
	 * 			| getAllDimensions().contains(dimension)
	 * @throws 	IllegalArgumentException
	 * 			One of the arguments is not a valid argument.
	 * 			| !isValidDimension()||!isValidSquare()
	 */
	@Model
	public void addSquare(E square, Dimension dimension){
		if(!isValidDimension(dimension))
			throw new IllegalArgumentException(dimension + " is not a valid dimension.");
		if(!isValidSquare(square))
			throw new IllegalArgumentException(square + " is not a valid square.");
		if(!hasValidNeighbours(dimension, square))
			throw new IllegalArgumentException("This square has several not proper neighbours.");
	}
	
	/**
	 * Checks whether this dimension is a valid dimension.
	 * 
	 * @param 	dimension
	 * 			The given dimension to check.
	 * @return	True if and only if the dimension is smaller than the maximumcoordinate, 
	 * 			if the x, y and z-value are not equal in this dungeon and all the containing dungeons 
	 * 			and if this dimension is still available.
	 * 		  	| result = !(dimension.getXPosition()>getMaxDimension().getXPosition() || 
	 * 			|	dimension.getYPosition()>getMaxDimension().getYPosition() || 
	 * 			|	dimension.getZPosition()>getMaxDimension().getZPosition()) && 
	 * 			|	!(dimension.getXPosition()==dimension.getYPosition() && 
	 * 			|	dimension.getZPosition()==dimension.getXPosition())&& (isAvailable(dimension)) &&
	 * 			|	getContainingDungeon().isValidDimension(dimension.add(getStartDimensionInContainingdungeon()
	 */
	 protected boolean isValidDimension(Dimension dimension){
		 if(dimension == null)
				return false;
		if(dimension.getXPosition()>getMaxDimension().getXPosition() || dimension.getYPosition()>getMaxDimension().getYPosition() || dimension.getZPosition()>getMaxDimension().getZPosition())
				return false;
		if(dimension.getXPosition()==dimension.getYPosition() && dimension.getZPosition()==dimension.getXPosition())
			return false;
		if(!isAvailable(dimension))
			return false;
		if(hasContainingDungeon())
			return getContainingDungeon().isValidDimension(dimension.add(getStartDimensionInContainingdungeon()));
		return true;
	}
	 
	 /**
	  * Checks whether the neighbours are valid, so that there are no walls near transparant squares.
	  * 
	  * @param 	dimension
	  * 		The dimension where the new square is coming.
	  * @param 	square
	  * 		The new square which is added.
	  * @return	True if and only if transparant squares don't have to share walls.
	  * 		| for each direction
	  * 		| 	if((getRootDungeon().getSquareAt(dimension.getDimensionIn(direction)).getBorderAt(direction.getOpposite()).getIsolation() || 
	  * 		| 			!getRootDungeon().getSquareAt(dimension.getDimensionIn(direction)).hasDoorAt(direction))
	  * 		|			&& (square instanceof TransparantSquare || getRootDungeon().getSquareAt(dimension.getDimensionIn(direction)) instanceof TransparantSquare))
	  * 		| 		result false
	  * 		| result = true
	  */
	 protected boolean hasValidNeighbours(Dimension dimension, E square){
		 for(Direction direction : Direction.values()){
			 Dimension newDimension = dimension.getDimensionIn(direction);
			 if (newDimension!= null){
				 Square next = getRootDungeon().getSquareAt(newDimension);
				 if(next != null){
					 if((next.getBorderAt(direction.getOpposite()).getIsolation() && !next.hasDoorAt(direction.getOpposite())) && (square instanceof TransparentSquare || next instanceof TransparentSquare))
						 return false;
				 }
			 }
		 }
		 return true;
	 }
	
	/**
	 * Checks whether the given dimension isn't used yet.
	 * 
	 * @param 	dimension
	 * 			The given dimension to verify.
	 * @return	True if and only if the dimension isn't used yet.
	 * 		  	| result = !getAllDimension().contains(dimension)
	 * @note	TIMECOMPLEXITY = O(1) because a hashset is used.
	 */
	public boolean isAvailable(Dimension dimension){
		return !getUsedDimensions().contains(dimension);
	}
	
	/**
	 * Checks whether the given square is a valid square.
	 * 
	 * @param	square
	 * 			The given square to check.
	 * @return	True if and only if the square is not used, not made of slippery material 
	 * 			if more than 20 percent of this dungeon has slippery floors.
	 * 			| if(square.isTerminated)
	 * 			|	return false
	 * 			| if(hasBeenUsed(square)
	 * 			| 	result = false
	 * 			| else if(square.getMadeOfSlipperyMaterial())
	 * 			| 	result = getSlipperyPercent() < 20 
	 * 			| else result = true
	 */
	protected boolean isValidSquare(E square){
		if(square == null){
			return false;
		}
		if(square.isTerminated())
			return false;
		if(hasBeenUsed(square)){
			return false;
		}
		if(square.getMadeOfSlipperyMaterial()){
			return (getSlipperyPercent()<20);
		}
		return true;
		}
	
	/**
	 * Return the percent of slippery floors in this dungeon.
	 * 
	 * @return	A percent of the slippery squares in this dungeon.
	 * 			| for each square in getAllSquares()
	 * 			| 	if(square.getMadeOfSlipperyMaterial())
	 * 			|		slipperiness++
	 * 			| slipperypercent = (slipperiness * 100)/(getAllSquares().size()+1)
	 */
	protected int getSlipperyPercent(){
		int slipperiness = 0;
		int count = 0;
		Condition condition = new DefaultCondition();
		Iterator<? extends E> it = iterator(condition);
		while(it.hasNext()){
			count ++;
			if(it.next().getMadeOfSlipperyMaterial())
				slipperiness++;
		}
		int slipperypercent = (slipperiness * 100) / (count+1);
		return slipperypercent;
	}
	
	/**
	 * Checker whether the given square has been used or not.
	 * 
	 * @param 	square
	 * 			The square to check.
	 * @return	True if and only if the given square hasn't already been used.
	 * 			| result = square.isUsed()
	 */
	public boolean hasBeenUsed(E square){
		return square.isUsed();
	}
	
	/**
	 * Sets the border of a border between two squares in the dungeon
	 * 
	 * @param 	square
	 * 			The first square.
	 * @param 	other
	 * 			The second square.
	 * @param 	direction
	 * 			The direction of the other square seen from the first square.
	 * @post	The first square and the second square share the same border with the highest isolation.
	 * 			| square.getBorderAt(direction).equals(other.getBorderAt(direction.other))
	 */
	protected void setBorder(E square, Square other, Direction direction){
		if(square instanceof Rock){
			square.getBorderAt(direction).setSquare(square);
		}
		else{
			((TeleportableSquare)square).setBorder(direction, square.getBorderAt(direction).getHighest(other.getBorderAt(direction.getOpposite())));
		}
		if(other instanceof Rock){
			Direction direction2= direction.getOpposite();
			((Rock)other).replaceBorderWith(other.getBorderAt(direction2), square.getBorderAt(direction).getHighest(other.getBorderAt(direction.getOpposite())));
			other.getBorderAt(direction2).setSquare(other);
		}
		else{
			((TeleportableSquare)other).setBorder(direction.getOpposite(), square.getBorderAt(direction).getHighest(other.getBorderAt(direction.getOpposite())));
		}
	}

	/**
	 * Updates the temperature of all the rocks in this dungeon.
	 * 
	 * @post 	All the rocks are now in a correct state.
	 * 			| for each rock in this dungeon.
	 * 			| getRoomOf(rock).hasCorrectTemperature() && getRoomOf(rock).hasCorrectHumidty()
	 */
	protected void updateRocks(){
		Condition condition = new RockCondition(new Temperature(-200), Rock.class);
		Iterator<? extends E> it = iterator(condition);
		while(it.hasNext()){
			getRoomOf(it.next()).updateTemperatureAndHumidity();
		}
	}
	
	/**
	 * Remove the given square at the given dimension.
	 * 
	 * @param	dimension
	 * 			The given dimension where the square must be removed.
	 * @post	The square at the given position isn't a part of the dungeon anymore.
	 * 		  	| getSquareAt(dimension)=null
	 */
	@Model
	public abstract void removeSquareAt(Dimension dimension);

	 /**
	  * Checks whether this dimension is a valid dimension for removing a square.
	  * 
	  * @param 	dimension
	  * 		The given dimension to check.
	  * @return	True if and only if the given dimension is smaller than the maximum dimension and if the given dimension contains a square.
	  * 		| result = !(dimension.getXPosition()>getMaxDimension().getXPosition() || dimension.getYPosition()>getMaxDimension().getYPosition() ||
	  * 		| dimension.getZPosition()>getMaxDimension().getZPosition())&&!IsAvailable(dimension)
	  */
	protected boolean isValidRemovableDimension(Dimension dimension){
		if(dimension == null)
			return false;
		if(dimension.getXPosition()>getMaxDimension().getXPosition() || dimension.getYPosition()>getMaxDimension().getYPosition() || dimension.getZPosition()>getMaxDimension().getZPosition())
			return false;
		return !isAvailable(dimension);
	}
	
	/**
	 * Closes a door in the given square in the given direction.
	 * 
	 * @param 	square
	 * 			The square which contains a door.
	 * @param 	direction
	 * 			The direction in the given square where the door stands.
	 * @post	The rooms on both sides of the door get separated if needed.
	 * @throws 	IllegalArgumentException
	 * 			The given square doesn't exist in this room.
	 * 			| getDimensionOf(square) == null
	 * @throws 	IllegalArgumentException
	 * 			There is no door in the given direction or the square isn't part of this dungeon.
	 * 			| !isDoor(square, direction)
	 */
	@Model
	public abstract void closeDoor(E square, Direction direction);
	
	/**
	 * Opens a door in the given square in the given direction.
	 * 
	 * @param 	square
	 * 			The square which contains a door.
	 * @param 	direction
	 * 			The direction in the given square where the door stands.
	 * @effect	Open the door in the subdungeon which contains  the given square.
	 * 			| getDungeonOf(square).openDoor(square,direction)
	 * @throws 	IllegalArgumentException
	 * 			The given square doesn't exist in this room.
	 * 			| getDimensionOf(square) == null
	 * @throws 	IllegalArgumentException
	 * 			There is no door in the given direction or the square isn't part of this dungeon.
	 * 			| !isDoor(square, direction)
	 */
	@Model
	public abstract void openDoor(E square, Direction direction);
	
	//////////////////////////
	//////OTHER CHECKERS//////
	//////////////////////////
	/**
	 * Checks whether the given square is reachable from the other given square.
	 * 
	 * @param	squareOne
	 * 			The departure square.
	 * @param	sqaureTwo
	 * 			The arrival square.
	 * @return	True if and only if squareTwo is reachable from squareOne.
	 * 			| if(squareTwo instanceof Rock)
	 * 			|	result = false
	 * 			| if(isInSameRoomAs(squareOne, squareTwo)
	 * 			| 	result = true
	 * 			| result = getAllReachableSquares(squareOne.contains(squareTwo))
	 * @throws	IllegalArgumentException
	 * 			One of the squares is zero.
	 * 			| squareOne==null || squareTwo==null
	 */
	public boolean isReachable(E squareOne, Square squareTwo) throws IllegalArgumentException{
		if(squareOne==null || squareTwo==null){
			throw new IllegalArgumentException("please do not use this method without proper input");
		}
		if(squareTwo instanceof Rock){
			return false;
		}
		if(isInSameRoomAs(squareOne,squareTwo))
			return true;
		return getAllReachableSquares(squareOne).contains(squareTwo);
	}
	
	/**
	 * Get all the squares which are reachable from the given square.
	 * 
	 * @param 	startSquare
	 * 			The departure square.
	 * @return	A set containing all the reachable squares.
	 * 			| result = for each TeleportableSquare (square), there exists an other TeleportableSquare(Other) such that 
	 * 			|	isReachable(startSquare, other) && (!square.getBorderAt(getDimensionOfSquare(square).getNeighbourDirection(getDimensionOfSquare(other))).getIsolation() ||
	 * 			|	other.canTeleportTo(square))
	 */
	private Set<Square> getAllReachableSquares(E startSquare){
		Set<Square> squares = new HashSet<Square>();
		List<Square> iterator = new ArrayList<Square>();
		iterator.add(startSquare);
		int i = 0;
		while(i < iterator.size()){
			for(Square other : iterator.get(i).getReachableSquares()){
				if(!iterator.contains(other)){
					iterator.add(other);
				}
			}
			i++;
		}
		squares.addAll(iterator);
		return squares;
	}
	
	/**
	 * checks whether the first square is in the same space as the second square.
	 * 
	 * @param	firstSquare 
	 * 			The first square.
	 * @param	otherSquare
	 * 			The second square.
	 * @return	True if and only the first square is in the same room as the second square.
	 * 		  	| result = room.contains(firstSquare)&& room.contains(secondSquare)
	 * @note	TIMECOMPLEXITY = O(m)+O(n) with n = number of squares in the room,
	 * 											m = number of squares in the dungeon.
	 */
	public boolean isInSameRoomAs(E firstSquare, Square otherSquare){
		Room<? extends E> room = getRoomOf(firstSquare);
		if(room!= null){
			if(firstSquare.getClass().isInstance(otherSquare)){
				if(room.contains(otherSquare))
					return true;
			}
			return isInSameWholeRoomAs(room,otherSquare);
		}
		
		return false;
	}
	
	/**
	 * Checks whether the other square is in the same whole room as the given room.
	 * 
	 * @param 	room
	 * `		The room to check whether the other square is in it or not.
	 * @param 	otherSquare
	 * 			The square to check whether he is in the given room or not.
	 * @return	True if and only if the square is in the whole room.
	 * 			| result = room.containsSquare(otherSquare)
	 */
	private boolean isInSameWholeRoomAs(Room<? extends E> room, Square otherSquare){
		return room.containsSquare(otherSquare);
	}
	
	////////////////////////////
	///////OTHER METHODS////////
	////////////////////////////
	/**
	 * Returns a string with information of this dungeon.
	 */
	public abstract String toString();
	
	/**
	 * Returns an iterator of all the squares of this dungeon limited with a given condition.
	 * 
	 * @param 	filter 
	 * 			The condition the squares have to satisfy to get returned.
	 * @return 	All the squares which satisfy the given condition
	 */
	public abstract Iterator<? extends E> iterator(final Condition filter);
}
